/*
 * Copyright (c) 2011, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package javax.security.auth.kerberos;

import java.io.File;
import java.security.AccessControlException;
import java.util.Objects;
import sun.security.krb5.EncryptionKey;
import sun.security.krb5.KerberosSecrets;
import sun.security.krb5.PrincipalName;
import sun.security.krb5.RealmException;

/**
 * This class encapsulates a keytab file.
 * <p>
 * A Kerberos JAAS login module that obtains long term secret keys from a
 * keytab file should use this class. The login module will store
 * an instance of this class in the private credential set of a
 * {@link javax.security.auth.Subject Subject} during the commit phase of the
 * authentication process.
 * <p>
 * If a {@code KeyTab} object is obtained from {@link #getUnboundInstance()}
 * or {@link #getUnboundInstance(java.io.File)}, it is unbound and thus can be
 * used by any service principal. Otherwise, if it's obtained from
 * {@link #getInstance(KerberosPrincipal)} or
 * {@link #getInstance(KerberosPrincipal, java.io.File)}, it is bound to the
 * specific service principal and can only be used by it.
 * <p>
 * Please note the constructors {@link #getInstance()} and
 * {@link #getInstance(java.io.File)} were created when there was no support
 * for unbound keytabs. These methods should not be used anymore. An object
 * created with either of these methods are considered to be bound to an
 * unknown principal, which means, its {@link #isBound()} returns true and
 * {@link #getPrincipal()} returns null.
 * <p>
 * It might be necessary for the application to be granted a
 * {@link javax.security.auth.PrivateCredentialPermission
 * PrivateCredentialPermission} if it needs to access the KeyTab
 * instance from a Subject. This permission is not needed when the
 * application depends on the default JGSS Kerberos mechanism to access the
 * KeyTab. In that case, however, the application will need an appropriate
 * {@link javax.security.auth.kerberos.ServicePermission ServicePermission}.
 * <p>
 * The keytab file format is described at
 * <a href="http://www.ioplex.com/utilities/keytab.txt">
 * http://www.ioplex.com/utilities/keytab.txt</a>.
 * <p>
 *
 * @since 1.7
 */
public final class KeyTab {

    /*
     * Impl notes:
     *
     * This class is only a name, a permanent link to the keytab source
     * (can be missing). Itself has no content. In order to read content,
     * take a snapshot and read from it.
     *
     * The snapshot is of type sun.security.krb5.internal.ktab.KeyTab, which
     * contains the content of the keytab file when the snapshot is taken.
     * Itself has no refresh function and mostly an immutable class (except
     * for the create/add/save methods only used by the ktab command).
     */

  // Source, null if using the default one. Note that the default name
  // is maintained in snapshot, this field is never "resolved".
  private final File file;

  // Bound user: normally from the "principal" value in a JAAS krb5
  // login conf. Will be null if it's "*".
  private final KerberosPrincipal princ;

  private final boolean bound;

  // Set up JavaxSecurityAuthKerberosAccess in KerberosSecrets
  static {
    KerberosSecrets.setJavaxSecurityAuthKerberosAccess(
        new JavaxSecurityAuthKerberosAccessImpl());
  }

  private KeyTab(KerberosPrincipal princ, File file, boolean bound) {
    this.princ = princ;
    this.file = file;
    this.bound = bound;
  }

  /**
   * Returns a {@code KeyTab} instance from a {@code File} object
   * that is bound to an unknown service principal.
   * <p>
   * The result of this method is never null. This method only associates
   * the returned {@code KeyTab} object with the file and does not read it.
   * <p>
   * Developers should call {@link #getInstance(KerberosPrincipal, File)}
   * when the bound service principal is known.
   *
   * @param file the keytab {@code File} object, must not be null
   * @return the keytab instance
   * @throws NullPointerException if the {@code file} argument is null
   */
  public static KeyTab getInstance(File file) {
    if (file == null) {
      throw new NullPointerException("file must be non null");
    }
    return new KeyTab(null, file, true);
  }

  /**
   * Returns an unbound {@code KeyTab} instance from a {@code File}
   * object.
   * <p>
   * The result of this method is never null. This method only associates
   * the returned {@code KeyTab} object with the file and does not read it.
   *
   * @param file the keytab {@code File} object, must not be null
   * @return the keytab instance
   * @throws NullPointerException if the file argument is null
   * @since 1.8
   */
  public static KeyTab getUnboundInstance(File file) {
    if (file == null) {
      throw new NullPointerException("file must be non null");
    }
    return new KeyTab(null, file, false);
  }

  /**
   * Returns a {@code KeyTab} instance from a {@code File} object
   * that is bound to the specified service principal.
   * <p>
   * The result of this method is never null. This method only associates
   * the returned {@code KeyTab} object with the file and does not read it.
   *
   * @param princ the bound service principal, must not be null
   * @param file the keytab {@code File} object, must not be null
   * @return the keytab instance
   * @throws NullPointerException if either of the arguments is null
   * @since 1.8
   */
  public static KeyTab getInstance(KerberosPrincipal princ, File file) {
    if (princ == null) {
      throw new NullPointerException("princ must be non null");
    }
    if (file == null) {
      throw new NullPointerException("file must be non null");
    }
    return new KeyTab(princ, file, true);
  }

  /**
   * Returns the default {@code KeyTab} instance that is bound
   * to an unknown service principal.
   * <p>
   * The result of this method is never null. This method only associates
   * the returned {@code KeyTab} object with the default keytab file and
   * does not read it.
   * <p>
   * Developers should call {@link #getInstance(KerberosPrincipal)}
   * when the bound service principal is known.
   *
   * @return the default keytab instance.
   */
  public static KeyTab getInstance() {
    return new KeyTab(null, null, true);
  }

  /**
   * Returns the default unbound {@code KeyTab} instance.
   * <p>
   * The result of this method is never null. This method only associates
   * the returned {@code KeyTab} object with the default keytab file and
   * does not read it.
   *
   * @return the default keytab instance
   * @since 1.8
   */
  public static KeyTab getUnboundInstance() {
    return new KeyTab(null, null, false);
  }

  /**
   * Returns the default {@code KeyTab} instance that is bound
   * to the specified service principal.
   * <p>
   * The result of this method is never null. This method only associates
   * the returned {@code KeyTab} object with the default keytab file and
   * does not read it.
   *
   * @param princ the bound service principal, must not be null
   * @return the default keytab instance
   * @throws NullPointerException if {@code princ} is null
   * @since 1.8
   */
  public static KeyTab getInstance(KerberosPrincipal princ) {
    if (princ == null) {
      throw new NullPointerException("princ must be non null");
    }
    return new KeyTab(princ, null, true);
  }

  // Takes a snapshot of the keytab content. This method is called by
  // JavaxSecurityAuthKerberosAccessImpl so no more private
  sun.security.krb5.internal.ktab.KeyTab takeSnapshot() {
    try {
      return sun.security.krb5.internal.ktab.KeyTab.getInstance(file);
    } catch (AccessControlException ace) {
      if (file != null) {
        // It's OK to show the name if caller specified it
        throw ace;
      } else {
        AccessControlException ace2 = new AccessControlException(
            "Access to default keytab denied (modified exception)");
        ace2.setStackTrace(ace.getStackTrace());
        throw ace2;
      }
    }
  }

  /**
   * Returns fresh keys for the given Kerberos principal.
   * <p>
   * Implementation of this method should make sure the returned keys match
   * the latest content of the keytab file. The result is a newly created
   * copy that can be modified by the caller without modifying the keytab
   * object. The caller should {@link KerberosKey#destroy() destroy} the
   * result keys after they are used.
   * <p>
   * Please note that the keytab file can be created after the
   * {@code KeyTab} object is instantiated and its content may change over
   * time. Therefore, an application should call this method only when it
   * needs to use the keys. Any previous result from an earlier invocation
   * could potentially be expired.
   * <p>
   * If there is any error (say, I/O error or format error)
   * during the reading process of the KeyTab file, a saved result should be
   * returned. If there is no saved result (say, this is the first time this
   * method is called, or, all previous read attempts failed), an empty array
   * should be returned. This can make sure the result is not drastically
   * changed during the (probably slow) update of the keytab file.
   * <p>
   * Each time this method is called and the reading of the file succeeds
   * with no exception (say, I/O error or file format error),
   * the result should be saved for {@code principal}. The implementation can
   * also save keys for other principals having keys in the same keytab object
   * if convenient.
   * <p>
   * Any unsupported key read from the keytab is ignored and not included
   * in the result.
   * <p>
   * If this keytab is bound to a specific principal, calling this method on
   * another principal will return an empty array.
   *
   * @param principal the Kerberos principal, must not be null.
   * @return the keys (never null, may be empty)
   * @throws NullPointerException if the {@code principal} argument is null
   * @throws SecurityException if a security manager exists and the read access to the keytab file
   * is not permitted
   */
  public KerberosKey[] getKeys(KerberosPrincipal principal) {
    try {
      if (princ != null && !principal.equals(princ)) {
        return new KerberosKey[0];
      }
      PrincipalName pn = new PrincipalName(principal.getName());
      EncryptionKey[] keys = takeSnapshot().readServiceKeys(pn);
      KerberosKey[] kks = new KerberosKey[keys.length];
      for (int i = 0; i < kks.length; i++) {
        Integer tmp = keys[i].getKeyVersionNumber();
        kks[i] = new KerberosKey(
            principal,
            keys[i].getBytes(),
            keys[i].getEType(),
            tmp == null ? 0 : tmp.intValue());
        keys[i].destroy();
      }
      return kks;
    } catch (RealmException re) {
      return new KerberosKey[0];
    }
  }

  EncryptionKey[] getEncryptionKeys(PrincipalName principal) {
    return takeSnapshot().readServiceKeys(principal);
  }

  /**
   * Checks if the keytab file exists. Implementation of this method
   * should make sure that the result matches the latest status of the
   * keytab file.
   * <p>
   * The caller can use the result to determine if it should fallback to
   * another mechanism to read the keys.
   *
   * @return true if the keytab file exists; false otherwise.
   * @throws SecurityException if a security manager exists and the read access to the keytab file
   * is not permitted
   */
  public boolean exists() {
    return !takeSnapshot().isMissing();
  }

  public String toString() {
    String s = (file == null) ? "Default keytab" : file.toString();
    if (!bound) {
      return s;
    } else if (princ == null) {
      return s + " for someone";
    } else {
      return s + " for " + princ;
    }
  }

  /**
   * Returns a hashcode for this KeyTab.
   *
   * @return a hashCode() for the {@code KeyTab}
   */
  public int hashCode() {
    return Objects.hash(file, princ, bound);
  }

  /**
   * Compares the specified Object with this KeyTab for equality.
   * Returns true if the given object is also a
   * {@code KeyTab} and the two
   * {@code KeyTab} instances are equivalent.
   *
   * @param other the Object to compare to
   * @return true if the specified object is equal to this KeyTab
   */
  public boolean equals(Object other) {
    if (other == this) {
      return true;
    }

    if (!(other instanceof KeyTab)) {
      return false;
    }

    KeyTab otherKtab = (KeyTab) other;
    return Objects.equals(otherKtab.princ, princ) &&
        Objects.equals(otherKtab.file, file) &&
        bound == otherKtab.bound;
  }

  /**
   * Returns the service principal this {@code KeyTab} object
   * is bound to. Returns {@code null} if it's not bound.
   * <p>
   * Please note the deprecated constructors create a KeyTab object bound for
   * some unknown principal. In this case, this method also returns null.
   * User can call {@link #isBound()} to verify this case.
   *
   * @return the service principal
   * @since 1.8
   */
  public KerberosPrincipal getPrincipal() {
    return princ;
  }

  /**
   * Returns if the keytab is bound to a principal
   *
   * @return if the keytab is bound to a principal
   * @since 1.8
   */
  public boolean isBound() {
    return bound;
  }
}
